package ru.artyushov.fingers.core.basic;

import ru.artyushov.fingers.core.FeatureExtractor;
import ru.artyushov.fingers.core.data.Fingerprint;
import ru.artyushov.fingers.core.data.Point;

import java.util.*;

public class SpecialPointsExtractor implements FeatureExtractor<SpecialPointsFeatureSet> {

    @Override
    public SpecialPointsFeatureSet extractFeatures(Fingerprint fingerprint) {
        Collection<Point> deadEnds = extractDeadEnds(fingerprint); //new LinkedList<Point>();
        Collection<Point> forks = extractForks(fingerprint); //new LinkedList<Point>();

        return new SpecialPointsFeatureSet(deadEnds, forks, fingerprint);
    }

    private int countNeighbors(Fingerprint fingerprint, int col, int row) {

        int neighbors = 0;

        for(int i = -1; i <= 1; i ++) {
            for(int j = -1; j <= 1; j ++) {

                if(i == 0 && j == 0) continue; // exclude the center

                if(fingerprint.isFilled(col + i, row + j)) {
                    neighbors ++;
                }
            }
        }

        return neighbors;
    }

    private List<Point> extractForks(Fingerprint fingerprint) {
        List<Point> result = new ArrayList<Point>();

        final int maskOffset = 2;
        boolean[][] data = fingerprint.getData();
        for (int i = maskOffset; i < fingerprint.getColNumber() - maskOffset; i++) {
            for (int j = maskOffset; j < fingerprint.getRowNumber() - maskOffset; j++) {

                // if point is not black or has less then 3 neighbours, then it's definitely not a fork
                if (!data[i][j] || countNeighbors(fingerprint, i, j) < 3) {
                    continue;
                }

                boolean[][] mask = createMask(data, i, j, maskOffset);
                boolean[][] smallMask = createMask(data, i, j, 1);
                if (countConnectedComponents(smallMask) >= 3 && countConnectedComponents(mask) >= 3) {
                    result.add(new Point(i, j));
                }
            }
        }

        return result;
    }

    private List<Point> extractDeadEnds(Fingerprint fingerprint) {
        List<Point> result = new ArrayList<Point>();

        final int maskOffset = 2;
        boolean[][] data = fingerprint.getData();
        for (int i = maskOffset; i < fingerprint.getColNumber() - maskOffset; i++) {
            for (int j = maskOffset; j < fingerprint.getRowNumber() - maskOffset; j++) {

                // if point is not black or has not exactly one neighbour, then it's definitely not a dead end
                if (!data[i][j] || countNeighbors(fingerprint, i, j) != 1) {
                    continue;
                }
                boolean[][] mask = createMask(data, i, j, maskOffset);
                if (countConnectedComponents(mask) == 1) {
                    result.add(new Point(i, j));
                }
            }
        }
        return result;
    }

    private int countConnectedComponents(boolean[][] data) {
        int currentColor = 0;
        int[][] colors = new int[data.length][data[0].length];
        for (int i = 0; i < data.length; i++) {
            for (int j = 0; j < data[0].length; j++) {
                if (!data[i][j] && colors[i][j] == 0) {
                    fillConnectedComponents(data, colors, i, j, ++currentColor);
                }
            }
        }
        return currentColor;
    }

    private void fillConnectedComponents(boolean[][] data, int[][] colors, int col, int row, int color) {
        colors[col][row] = color;
        if (col > 0 && !data[col - 1][row] && colors[col - 1][row] == 0) fillConnectedComponents(data, colors, col - 1, row, color);
        if (row > 0 && !data[col][row - 1] && colors[col][row - 1] == 0) fillConnectedComponents(data, colors, col, row - 1, color);
        if (col < data.length - 1 && !data[col + 1][row] && colors[col + 1][row] == 0) fillConnectedComponents(data, colors, col + 1, row, color);
        if (row < data[0].length - 1 && !data[col][row + 1] && colors[col][row + 1] == 0) fillConnectedComponents(data, colors, col, row + 1, color);
    }

    private boolean[][] createMask(boolean[][] data, int col, int row, int radius) {
        boolean[][] result = new boolean[2 * radius + 1][2 * radius + 1];
        for (int k = -radius; k <= radius; k++) {
            for (int l = -radius; l <= radius; l++) {
                result[k + radius][l + radius] = data[col + k][row + l];
            }
        }
        return result;
    }
}
